Au travail aujourd'hui, on m'a demandé d'ajouter une forme d'authentification à l'une de nos applications Web. L'application elle-même n'avait pas d'authentification intégrée et permettait aux utilisateurs de soumettre des URL et des fichiers pour analyse. De toute évidence, nous ne pouvions pas mettre cette application Web directement en production, car elle aurait presque immédiatement été utilisée de manière abusive par nos utilisateurs. Heureusement, je savais déjà que l'on pouvait utiliser Nginx comme proxy inverse, en ajoutant l'authentification à presque tout.
L'utilisation de Nginx comme reverse proxy est bien documentée, et l'ajout d'une authentification de base consiste simplement à écrire les mots de passe dans un fichier, puis à modifier votre configuration.
Mais que faire si vous avez besoin de quelque chose de plus robuste ?
Mon patron n'aimait pas l'idée que les gens puissent deviner ou forcer les informations d'identification des utilisateurs, et je ne voulais pas avoir à me connecter au proxy et à modifier un fichier chaque fois que nous avions besoin d'un nouvel utilisateur ou qu'un mot de passe devait être modifié. Il devait y avoir quelque chose de mieux.
Je me suis mis à la recherche d'un frontal d'authentification, avec la possibilité de provisionner de nouveaux utilisateurs.
C'est là que Arno0x’s Two Factor Auth est entré en jeu. Il fournit un joli frontal pour gérer et créer des comptes d'authentification, tout en fournissant également un backend pour permettre à nginx de s'assurer que l'utilisateur actuel est authentifié.
Le téléchargement et la configuration du service sont assez simples. Mon fichier de configuration 2FA était simple, j'ai juste eu à suivre le readme sur github.
La configuration de Nginx a nécessité un peu plus de doigté. J'ai finalement réussi à la faire fonctionner avec la configuration suivante :
map $request_uri $loggable {
/submit/api/submit 1;
default 0;
}
log_format phpcookie '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" - "$http_cookie"';
server {
root /var/www/;
server_name example.site;
index index.php;
error_page 401 = @error401;
location @error401 {
return 302 $scheme://$host/twofactorauth/login/login.php?from=$uri;
}
location = /twofactorauth/nginx/auth.php {
include /etc/nginx/fastcgi.conf;
fastcgi_param CONTENT_LENGTH "";
fastcgi_pass unix:/run/php/php7.2-fpm.sock;
}
location /twofactorauth/ {
index index.php;
}
location /twofactorauth/db/ {
deny all;
}
location = /twofactorauth/login/login.php {
allow all;
auth_request off;
include /etc/nginx/fastcgi.conf;
fastcgi_pass unix:/run/php/php7.2-fpm.sock;
}
location ~ \.php {
include /etc/nginx/fastcgi.conf;
fastcgi_pass unix:/run/php/php7.2-fpm.sock;
}
location /static/ {
alias /var/www/html/static/;
}
location / {
access_log /var/log/nginx/cuckoo.log phpcookie if=$loggable;
proxy_pass http://127.0.0.1:8080;
auth_request /twofactorauth/nginx/auth.php;
proxy_buffering off;
}
}
La majorité de la configuration ici est de gérer ce que le reverse proxy sert lui-même, et ce qu'il passe à notre webapp.
map $request_uri $loggable {
/submit/api/submit 1;
default 0;
}
log_format phpcookie '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" - "$http_cookie"';
Ce code gère la façon dont la journalisation se produit. Je voulais enregistrer des requêtes spécifiques au proxy, donc j'ai utilisé ces deux paragraphes.
La première section définit les requêtes qui peuvent être considérées comme "enregistrables". Les requêtes avec la terminaison 1 ; sont considérées comme candidates à la journalisation, la valeur par défaut étant "ne pas journaliser". Si les chiffres 0 et 1 étaient inversés, il s'agirait plutôt d'une opération de mise sur liste noire.
La deuxième section gère le format que je veux que le journal utilise. Dans ce cas, j'ai choisi d'inclure l'option $http_cookie. Cela me permet de voir quel utilisateur a fait la demande.
server {
root /var/www/;
server_name example.site;
index index.php;
error_page 401 = @error401;
location @error401 {
return 302 $scheme://$host/twofactorauth/login/login.php?from=$uri;
}
Cette section définit le répertoire racine que nginx doit utiliser, le nom du serveur auquel il doit répondre et le fichier à charger lorsque https://example.site/ est demandé.
Cette section indique également à nginx ce qu'il doit faire si l'utilisateur n'a pas encore été authentifié. Dans ce cas, au lieu de renvoyer une erreur 401, l'utilisateur est redirigé vers la page de connexion 2FA, avec la page initialement demandée incluse dans l'URI. Une fois l'utilisateur authentifié, il sera redirigé vers cette URI.
location = /twofactorauth/nginx/auth.php {
include /etc/nginx/fastcgi.conf;
fastcgi_param CONTENT_LENGTH "";
fastcgi_pass unix:/run/php/php7.2-fpm.sock;
}
location ~ \.php {
include /etc/nginx/fastcgi.conf;
fastcgi_pass unix:/run/php/php7.2-fpm.sock;
}
location = /twofactorauth/login/login.php {
allow all;
auth_request off;
include /etc/nginx/fastcgi.conf;
fastcgi_pass unix:/run/php/php7.2-fpm.sock;
}
location /twofactorauth/ {
index index.php;
}
location /twofactorauth/db/ {
deny all;
}
Ici, j'ai dit à nginx comment traiter les fichiers avec des extensions .php. En gros, je fais savoir à nginx que n'importe lequel de ces fichiers doit être exécuté par php7.2-fpm. Rien de vraiment spécial ici.
auth_request off ; spécifie que l'accès à la page de connexion 2FA n'est pas soumis à l'authentification. (si c'était le cas, nous ne pourrions jamais nous connecter).
J'ai également défini le fichier d'index pour les requêtes vers https://example.site/twofactoauth/ et spécifié que tout accès au dossier de la base de données 2FA est refusé à tous.
location /static/ {
alias /var/www/html/static/;
}
location / {
access_log /var/log/nginx/cuckoo.log phpcookie;
#access_log /var/log/nginx/cuckoo.log phpcookie if=$loggable;
proxy_pass http://127.0.0.1:8080;
auth_request /twofactorauth/nginx/auth.php;
proxy_buffering off;
}
}
La section supérieure du code garantit que le contenu statique est servi par le proxy et n'est pas transmis à l'application web principale. Cela permet de s'assurer que les ressources les plus importantes ne sont pas tirées sur une connexion à faible latence.
Ensuite, nous indiquons que nous voulons qu'un nouveau fichier journal soit créé, en utilisant le format phpcookie précédemment défini et validé par rapport à notre liste blanche de journaux précédemment définie.
Je configure ensuite la destination du proxy inverse pour qu'il soit notre port local 8080, et je demande une authentification pour cet accès.
La mise en mémoire tampon du proxy est désactivée, car ma WebApp utilise des données en continu et la mise en mémoire tampon peut introduire des délais de rupture dans ce flux.
Et c'est tout ! Une implémentation 2FA pour une application Web sans authentification !